package com.xingchi.tornado.storage.core.supports;

import com.xingchi.tornado.basic.ErrorCode;
import com.xingchi.tornado.exception.BizException;
import com.xingchi.tornado.storage.core.AbstractFileStorageService;
import com.xingchi.tornado.storage.core.config.StorageProperties;
import com.xingchi.tornado.storage.core.exception.FileUploadException;
import com.xingchi.tornado.storage.core.model.dto.FileChunkMergeDTO;
import com.xingchi.tornado.storage.core.model.dto.FileChunkUploadDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.FileCopyUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * 本地存储服务
 *
 * @author xingchi
 * @date 2024/4/21 20:50
 */
@Slf4j
public class LocalFileStorageService extends AbstractFileStorageService {

    private final String filePath;
    private final String routePrefix;
    private final String domain;

    private final StorageProperties.LocalProperties localProperties;

    public LocalFileStorageService(StorageProperties.LocalProperties localProperties) {

        if (StringUtils.isBlank(localProperties.getFilePath())) {
            throw new FileUploadException("LOCAL 存储服务初始化失败，请配置文件上传路径");
        }

        this.filePath = localProperties.getFilePath();
        this.localProperties = localProperties;
        if (StringUtils.isBlank(localProperties.getRoutePrefix())) {
            this.routePrefix = "/storage";
        } else {
            String routePrefix = localProperties.getRoutePrefix();
            // 如果routePrefix以/结尾，则去掉，如果开头没有/则加上
            if (!routePrefix.startsWith("/")) {
                routePrefix = "/" + routePrefix;
            }
            this.routePrefix = StringUtils.removeEnd(routePrefix, "/");
        }

        if (StringUtils.isBlank(localProperties.getDomain())) {
            throw new FileUploadException("LOCAL 存储服务初始化失败，请指定访问域名");
        }
        this.domain = StringUtils.removeEnd(localProperties.getDomain(), "/");

        log.info("local file storage service init successfully!");
    }

    /**
     * 根据文件的输入流上传文件
     *
     * @param is       文件的输入流
     * @param fileName 文件的完整名称
     * @return 文件在文件存储服务器上的路径唯一标识，在云存储中返回文件的对象名
     */
    @Override
    public String uploadFile(InputStream is, String fileName) {
        String path = getPath(fileName);
        String savePath = getPath(filePath, path);

        try {
            File file = new File(savePath);
            File parentFile = file.getParentFile();
            if (!parentFile.exists() && !parentFile.mkdirs()) {
                log.warn("文件上传失败，无法创建对应的文件路径");
                throw new FileUploadException("文件上传失败");
            }
            FileCopyUtils.copy(is, Files.newOutputStream(file.toPath()));
        } catch (IOException e) {
            log.warn("文件上传失败，原因：{}", e.getMessage(), e);
            throw new FileUploadException("文件上传失败");
        }

        return path;
    }

    /**
     * 根据文件在文件服务器上的存储路径获取文件对应的字节书，注意文件过大可能会导致内存溢出问题
     *
     * @param path 文件的存储路径
     * @return 文件字节
     */
    @Override
    public byte[] getFileBytes(String path) {
        File oigFile = new File(filePath + path);

        try {
            return FileCopyUtils.copyToByteArray(oigFile);
        } catch (IOException e) {
            log.warn("文件转换失败，原因：{}", e.getMessage(), e);
            return EMPTY_BYTES;
        }
    }

    @Override
    public InputStream getInputStream(String path) {
        try {
            return Files.newInputStream(Paths.get(filePath, path));
        } catch (IOException e) {
            log.error("获取文件失败，原因：{}", e.getMessage(), e);
            throw BizException.of(ErrorCode.FILE_STREAM_GET_FAILED, e.getMessage());
        }
    }

    /**
     * 删除文件
     *
     * @param path 文件的存储路径
     */
    @Override
    public void deleteFile(String path) {
        try {
            FileUtils.deleteQuietly(new File(filePath + path));
        } catch (Exception e) {
            log.warn("删除文件异常", e);
        }
    }

    @Override
    protected boolean isChunkExists(String chunkPath) {
        return new File(filePath + chunkPath).exists();
    }

    @Override
    protected void storeChunk(FileChunkUploadDTO info, String chunkPath) {
        try {
            File chunk = getChunkFile(chunkPath);
            Files.write(chunk.toPath(), info.getContent());
        } catch (IOException e) {
            log.error("分片上传失败，原因：{}", e.getMessage(), e);
            throw new FileUploadException("分片上传失败");
        }
    }

    /**
     * 获取分片文件，如果对应文件的父目录不存在则先创建出其父目录
     *
     * @param chunkPath 分片路径
     * @return 分片文件
     */
    private File getChunkFile(String chunkPath) {
        File chunk = new File(filePath + chunkPath);
        File parent = chunk.getParentFile();
        if (!parent.exists()) {
            boolean mkdirs = parent.mkdirs();
            if (!mkdirs) {
                log.warn("分片上传失败，无法创建对应的文件路径");
                throw new FileUploadException("分片上传失败");
            }
        }
        return chunk;
    }

    @Override
    protected String combineChunks(FileChunkMergeDTO info, String targetPath) {
        try {
            File chunkFile = getChunkFile(targetPath);

            try (FileOutputStream fileOutputStream = new FileOutputStream(chunkFile)) {
                FileChannel outChannel = fileOutputStream.getChannel();
                String chunkPath = getChunkPath(info.getSha256());

                // 按顺序合并分片
                for (int i = 1; i <= info.getTotalChunks(); i++) {
                    File chunk = new File(filePath + chunkPath + i + ".chunk");
                    try (FileInputStream fileInputStream = new FileInputStream(chunk);) {
                        FileChannel inChannel = fileInputStream.getChannel();
                        inChannel.transferTo(0, inChannel.size(), outChannel);
                    }
                }
            }

            // 清理分片文件
            FileUtils.deleteDirectory(new File(filePath + getChunkPath(info.getSha256())));
            return targetPath;
        } catch (IOException e) {
            log.error("分片合并文件失败，原因：{}", e.getMessage(), e);
            throw new FileUploadException("分片合并失败");
        }
    }

    @Override
    public String getUrl(String path) {
        return domain + routePrefix + super.getUrl(path);
    }
}
